// SPDX-License-Identifier: BSD-2.0+
/*
 * Copyright (C) 2019 Tang Haifeng <tanghaifeng-gz@loongson.cn>
 *
 * Basic support for the pwm module on loongson.
 */

#include <common.h>
#include <div64.h>
#include <dm.h>
#include <pwm.h>
#include <asm/io.h>

#if defined(CONFIG_MACH_LOONGSON1)
#include <mach/ls1x.h>
#define LS_PWM0_BASE LS1X_PWM0_BASE
#define LS_PWM1_BASE LS1X_PWM1_BASE
#define LS_PWM2_BASE LS1X_PWM2_BASE
#define LS_PWM3_BASE LS1X_PWM3_BASE
#elif defined(CONFIG_MACH_LOONGSON2_SOC)
#include <mach/ls2k.h>
#define LS_PWM0_BASE (CKSEG1ADDR(LS2X_PWM0_BASE))
#define LS_PWM1_BASE (CKSEG1ADDR(LS2X_PWM1_BASE))
#define LS_PWM2_BASE (CKSEG1ADDR(LS2X_PWM2_BASE))
#define LS_PWM3_BASE (CKSEG1ADDR(LS2X_PWM3_BASE))
#elif defined(CONFIG_SB_LS7A1000)
#include <mach/ls7a.h>
#define LS_PWM0_BASE (CKSEG1ADDR(LS7A_PWM0_BASE))
#define LS_PWM1_BASE (CKSEG1ADDR(LS7A_PWM1_BASE))
#define LS_PWM2_BASE (CKSEG1ADDR(LS7A_PWM2_BASE))
#define LS_PWM3_BASE (CKSEG1ADDR(LS7A_PWM3_BASE))
#endif

#define REG_PWM_CNTR	0x00
#define REG_PWM_HRC	0x04
#define REG_PWM_LRC	0x08
#define REG_PWM_CTRL	0x0c

static void loongson_pwm_mux(void __iomem *base)
{
#if defined(CONFIG_MACH_LOONGSON2_SOC)
	{
	void __iomem *mux = (void __iomem *)CKSEG1ADDR(LS2X_MUX_BASE);
	int offset = 0;
	int id = ((u32)(base - LS_PWM0_BASE)) >> 4;

	switch (id) {
	case 0:
		offset = 12;
		break;
	case 1:
		offset = 13;
		break;
	case 2:
		offset = 14;
		break;
	case 3:
		offset = 15;
		break;
	default:
		return;
		break;
	}
	writel(readl(mux) | (1 << offset), mux);
	}
#endif
}

/* pwm_id from 0..7 */
static void __iomem *pwm_id_to_reg(int pwm_id)
{
	switch (pwm_id) {
	case 0:
		return (void __iomem *)LS_PWM0_BASE;
	case 1:
		return (void __iomem *)LS_PWM1_BASE;
	case 2:
		return (void __iomem *)LS_PWM2_BASE;
	case 3:
		return (void __iomem *)LS_PWM3_BASE;
	default:
		printf("unknown pwm_id: %d\n", pwm_id);
		break;
	}
	return NULL;
}

static int loongson_pwm_config(void __iomem *base, int duty_ns, int period_ns)
{
	unsigned long long tmp;
	unsigned long long period, duty;
	u32 reg;

	reg = readl(base + REG_PWM_CTRL);
	reg = reg & (~(1<<0));
	writel(reg, base + REG_PWM_CTRL);

	if (duty_ns < 0 || duty_ns > period_ns)
		return -EINVAL;

	tmp = (unsigned long long)gd->bus_clk * period_ns;
	do_div(tmp, 1000000000);
	period = tmp;

	tmp = (unsigned long long)period * duty_ns;
	do_div(tmp, period_ns);
	duty = period - tmp;

	writel(duty, base + REG_PWM_HRC);
	writel(period, base + REG_PWM_LRC);

	reg = readl(base + REG_PWM_CTRL);
	reg = reg | (1<<0);
	writel(reg, base + REG_PWM_CTRL);

	return 0;
}

int pwm_init(int pwm_id, int div, int invert)
{
	void __iomem *base = (void __iomem *)pwm_id_to_reg(pwm_id);
	u32 reg;

	if (!base)
		return -1;

	loongson_pwm_mux(base);

	reg = readl(base + REG_PWM_CTRL);

	if (invert)
		reg |= (1<<9);
	else
		reg &= (~(1<<9));

	writel(reg, base + REG_PWM_CTRL);

	return 0;
}

int pwm_config(int pwm_id, int duty_ns, int period_ns)
{
	void __iomem *base = (void __iomem *)pwm_id_to_reg(pwm_id);

	if (!base)
		return -1;

	loongson_pwm_config(base, duty_ns, period_ns);

	return 0;
}

int pwm_enable(int pwm_id)
{
	void __iomem *base = (void __iomem *)pwm_id_to_reg(pwm_id);
	u32 reg;

	if (!base)
		return -1;

	writel(0x00, base + REG_PWM_CNTR);
	reg = readl(base + REG_PWM_CTRL);
	reg = reg & (~(1<<3));
	writel(reg, base + REG_PWM_CTRL);

	return 0;
}

void pwm_disable(int pwm_id)
{
	void __iomem *base = (void __iomem *)pwm_id_to_reg(pwm_id);
	u32 reg;

	if (!base)
		return;

	reg = readl(base + REG_PWM_CTRL);
	reg = reg | (1<<3) | (1<<0);
	writel(reg, base + REG_PWM_CTRL);
}

#if defined(CONFIG_DM_PWM)
struct loongson_pwm_priv {
	void __iomem *base;
	bool invert;
};

static int loongson_pwm_set_invert(struct udevice *dev, uint channel,
			      bool polarity)
{
	struct loongson_pwm_priv *priv = dev_get_priv(dev);
	u32 reg;

	debug("%s: polarity=%u\n", __func__, polarity);
	priv->invert = polarity;

	reg = readl(priv->base + REG_PWM_CTRL);

	if (polarity)
		reg |= (1<<9);
	else
		reg &= (~(1<<9));

	writel(reg, priv->base + REG_PWM_CTRL);

	return 0;
}

static int loongson_pwm_set_config(struct udevice *dev, uint channel,
			      uint period_ns, uint duty_ns)
{
	struct loongson_pwm_priv *priv = dev_get_priv(dev);

	debug("%s: Config '%s' channel: %d\n", __func__, dev->name, channel);

	loongson_pwm_config(priv->base, duty_ns, period_ns);

	return 0;
};

static int loongson_pwm_set_enable(struct udevice *dev, uint channel, bool enable)
{
	struct loongson_pwm_priv *priv = dev_get_priv(dev);
	u32 reg;

	debug("%s: Enable '%s' state: %d\n", __func__, dev->name, enable);

	if (enable) {
		writel(0x00, priv->base + REG_PWM_CNTR);
		reg = readl(priv->base + REG_PWM_CTRL);
		reg = reg & (~(1<<3));
		writel(reg, priv->base + REG_PWM_CTRL);
	} else {
		reg = readl(priv->base + REG_PWM_CTRL);
		reg = reg | (1<<3) | (1<<0);
		writel(reg, priv->base + REG_PWM_CTRL);
	}

	return 0;
};

static int loongson_pwm_ofdata_to_platdata(struct udevice *dev)
{
	struct loongson_pwm_priv *priv = dev_get_priv(dev);

	priv->base = (void __iomem *)devfdt_get_addr(dev);

	return 0;
}

static int loongson_pwm_probe(struct udevice *dev)
{
	return 0;
}

static const struct pwm_ops loongson_pwm_ops = {
	.set_invert	= loongson_pwm_set_invert,
	.set_config	= loongson_pwm_set_config,
	.set_enable	= loongson_pwm_set_enable,
};

static const struct udevice_id loongson_pwm_ids[] = {
	{ .compatible = "loongson,ls1x-pwm" },
	{ .compatible = "loongson,ls2x-pwm" },
	{ }
};

U_BOOT_DRIVER(loongson_pwm) = {
	.name	= "loongson_pwm",
	.id	= UCLASS_PWM,
	.of_match = loongson_pwm_ids,
	.ops	= &loongson_pwm_ops,
	.ofdata_to_platdata	= loongson_pwm_ofdata_to_platdata,
	.probe		= loongson_pwm_probe,
	.priv_auto_alloc_size	= sizeof(struct loongson_pwm_priv),
};
#endif
